/*--------------------------
------- TRUCHY Romain  -------
------- MARENCO Rémi -------
-------     5IRC     -------  
----------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <unistd.h> 
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <sys/types.h> 
#include <dirent.h> 
#include <sys/stat.h> 
#include <time.h> 
#include "trame.h"
#include "proto.h"
#define BUFFLEN 16
#define STAT_LENTGH 10

static int num = -1;
static int num_trame = 0;
static int sockfd, addr_in_size;
static struct sockaddr_in *to, *my_addr, *from;
static int nbErreur = 10;
static int tab_historique[STAT_LENTGH] = {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, }; // tableau des 10 dernieres trames valides recues.
static int indice = 0;
static int pid;

/*
 * Fonction permettant de gérer la détection des flags et des numéros de trame
 * Renvoi la trame si elle passe tous les tests
 */
trame* gererFlux(trame* t){
	
	int exists = 0;
	int max = 0;
	int i = 0;
	int numTrame = t->i;
	indice = indice % STAT_LENTGH;

	// On parcourt le tableau pour détecter les erreurs
	for(i = 0; i < STAT_LENTGH; i++)
	{
		if(tab_historique[i] == numTrame)
		{// si la trame est un doublon
			exists = 1;
			//printf("(Doublon de verification : %s | type : %d | taille : %d | num trame : %d)\n", t->mess,t->type, t->taille, t->i);
		}
		if(tab_historique[i] > tab_historique[max])
		{
			max = i;
		}
	}
	if(exists == 1) //si c'est un doublon
	{
		nbErreur--;
	}
	else //sinon on retourne la trame pour interpretation
	{
		if(numTrame > tab_historique[max])
		{
			if(t->type!=LISTCL && t->type!=LISTSRV){
				tab_historique[indice] = numTrame;
				indice++;
			}
			return t;
		}
		else
		{
			printf("Erreur dans l'envoi de la trame...");
			nbErreur++;
		}	
	}
	if(nbErreur < 0) nbErreur = 0;
	
	//Gestion de la congestion
	/*if(rand()%10 > 7 && numTrame > 10)
	{
		if((float)nbErreur/(float)numTrame > 0.33 )
		{
			printf("Ralentir\n");
			envoyerTrame(creerTrame("",""), SLOW);    
		}
		else if((float)nbErreur/(float)numTrame <= 0.05)
		{
			printf("Accelerer\n");
			envoyerTrame(creerTrame("",""), FAST);
		}
	}
	printf("ERREUR (nbErreur = %d et rapport = %3f) \n", nbErreur, (float)nbErreur/(float)numTrame);*/
	return NULL;
}

/*
 * Fonction permettant de récupérer un fichier et de l'envoyer 
 */
void getFichier(trame* t)
{
	char nomFich[80];
	FILE* fp;
	int origInvers;

	//printf("On rentre dans getFichier, origine : %d\n", t->orig);
	if(t->orig==SERVER)
	{
		origInvers = CLIENT;
		strcpy(nomFich,"./ClientContent/");
	}
	if(t->orig==CLIENT)
	{	
		origInvers = SERVER;
		strcpy(nomFich,"./ServerContent/");
	}
	strcat(nomFich,t->name);
	if ((fp = fopen(nomFich,"rb")) == NULL)
	{
		printf("Impossible d'ouvrir le fichier données en lecture : %s\n", t->name);
		envoyerTrame(creerTrame("Fichier non trouvé ou impossible de l'ouvrir","Probleme fichier",0, origInvers), ERROR);
		return;
	}
	else //fichier existant 
	{
		//envoi de la taille du fichier
		fseek(fp, 0, SEEK_END);//placement en fin de fichier
		long taille = ftell(fp);
		//printf("TAILLE : %ld\n", taille);


		rewind(fp);//retour au debut du fichier
		//envoi du contenu du fichier
		char buffer_fichier[BUFFLEN];
		int taille_cumule = 0;
		int n2;
		while (taille_cumule <taille)
		{
			n2 = fread(buffer_fichier, 1, BUFFLEN-1, fp);
			envoyerTrame(creerTrame(buffer_fichier,t->name,n2, origInvers), SEND);    
			taille_cumule = n2 + taille_cumule;
			printf("TAILLE CUMULEE : %d n2 = %d, buffer = %s \n", taille_cumule, n2,buffer_fichier);
		}
		envoyerTrame(creerTrame("EndFile","EndFile",0, origInvers), END);
		printf("Fin d'envoi du fichier!\n");    
	}
	fclose(fp);
}

void sendFichier(char* fichChoix)
{
	char nomFich[80];
	FILE* fp;
	int origine = CLIENT;

	//printf("On rentre dans sendFichier\n");

	strcpy(nomFich,"./ClientContent/");

	strcat(nomFich,fichChoix);
	if ((fp = fopen(nomFich,"rb")) == NULL)
	{
		printf("Impossible d'ouvrir le fichier données en lecture : %s\n", fichChoix);
		envoyerTrame(creerTrame("Fichier non trouvé ou impossible de l'ouvrir","Probleme fichier",0, origine), ERROR);
		return;
	}
	else //fichier existant 
	{
		//envoi de la taille du fichier
		fseek(fp, 0, SEEK_END);//placement en fin de fichier
		long taille = ftell(fp);
		//printf("TAILLE : %ld\n", taille);


		rewind(fp);//retour au debut du fichier
		//envoi du contenu du fichier
		char buffer_fichier[BUFFLEN];
		int taille_cumule = 0;
		int n2;
		while (taille_cumule <taille)
		{
			n2 = fread(buffer_fichier, 1, BUFFLEN-1, fp);
			envoyerTrame(creerTrame(buffer_fichier,fichChoix,n2, origine), SEND);    
			taille_cumule = n2 + taille_cumule;
			printf("TAILLE CUMULEE : %d n2 = %d, buffer = %s \n", taille_cumule, n2,buffer_fichier);
		}
		envoyerTrame(creerTrame("EndFile","EndFile",0, origine), ENDFILE);
		printf("Fin d'envoi du fichier!\n");
		decoClient();    
	}
	fclose(fp);
}

/*
 * Fonction permettant de lister le contenu du répertoire ServerContent et de renvoyer ce contenu dans une trame
 */
void listeContenuServeur()
{
	FILE* fp;
	char path[1035];
	/* Open the command for reading. */
	fp = popen("/bin/ls ./ServerContent/", "r");
	if (fp == NULL) {
	  printf("Failed to run command\n" );
	  exit;
	}

	/* Read the output a line at a time - output it. */
	while (fgets(path, sizeof(path)-1, fp) != NULL) {
	  //on envoie ligne à ligne le contenu du serveur
	  envoyerTrame(creerTrame(path,"",0, SERVER), LISTACK);
	  //printf("%s", path);
	}

	/* close */
	//Envoie de l'information de fin de liste
	envoyerTrame(creerTrame("EndList","EndList",0, SERVER), ENDLIST);
	pclose(fp);
}


void listeContenuClient()
{
	FILE* fp;
	char path[1035];
	/* Open the command for reading. */
	fp = popen("/bin/ls ./ClientContent/", "r");
	if (fp == NULL) {
	  printf("Failed to run command\n" );
	  exit;
	}

	/* Read the output a line at a time - output it. */
	while (fgets(path, sizeof(path)-1, fp) != NULL) {
	  //on envoie ligne à ligne le contenu du client
	  //envoyerTrame(creerTrame(path,"",0, CLIENT), LISTACK);
	  printf("%s", path);
	}

	/* close */
	//Envoie de l'information de fin de liste
	//envoyerTrame(creerTrame("EndList","EndList",0, CLIENT), ENDLIST);
	pclose(fp);
}

/*
 * Fonction permettant de stocker le morceau de fichier reçu
 */
void stockeFichier(trame* t)
{
	char nomFich[80];
	FILE* fp;

	strcpy(nomFich,"./Downloads/");
	strcat(nomFich,t->name);
	fp = fopen(nomFich, "ab+"); // Ouverture ou création du morceau reçu en mode ajout à la fin du fichier
	if(fwrite( t->mess , 1 , t->taille , fp) < 1)
	{
		printf("ERREUR ECRITURE !!");
	}
	
	fclose(fp);
}

/*
* Fonction permettant de réinitialiser le serveur pour l'acceptation d'un nouveau client
*/
void reinitServeur()
{
	int i;
	num_trame = 0;
	for(i = 0; i < STAT_LENTGH; i++)
	{
		tab_historique[i] = -1;
	}
}

/*
 * Fonction protocole permettant de gérer tout l'aspect des échanges
 */
void protocole(trame* t){
	// Déclaration des variables
	FILE* fp;
	int status;

	// Si la trame n'est pas null
	if(t != NULL)
	{
		num = t->i;
		if(t->type!=LISTACK)
		{
			//printf("reception : %s | type : %d | taille : %d | num trame : %d\n", t->mess,t->type, t->taille, t->i);
		}
		
		switch (t->type)
		{
			case GET: // Si le type de la trame reçue est un type GET, on récupère le fichier
				//printf("Je suis GET\n");
				getFichier(t);
				break;
			case LISTSRV: // Si le type de la trame reçue est un type LISTSRV, on va lister le contenu du serveur, et le renvoyer
				//printf("Je suis LISTSRV\n");
				listeContenuServeur();
				break;
			case LISTCL: // Si le type de la trame reçue est un type LISTCL, on va lister le contenu du client, et le renvoyer
				//printf("Je suis LISTCL\n");
				listeContenuClient();
				break;
			case LISTACK: //on recoit le contenu du serveur et on l'affiche
				// printf("Je suis LISTACK\n");
				printf("%s",t->mess); // Affichage du message de la trame
				break;
			case ERROR: //le fichier n'existe pas
				// printf("Je suis ERROR\n");
				// printf("Erreur de fichier\n");
				printf("Erreur : %s\nMessage : %s\n", t->name, t->mess);
				printf("Retour au menu dans 2 secondes...:\n");
				printf("0\n");
				sleep(1);
				printf("1\n");
				sleep(1);
				printf("2\n");
				menu();
				break;
			case ENDLIST: //le fils chargé de lister le serveur a fini et se tue
				// printf("Je suis ENDLIST\n");
				if(pid==0)
				  exit(0);
				break;
			case SEND: // on recoit paquet à paquet le fichier demandé
				stockeFichier(t);
				// printf("Je suis SEND\n");
				break;
			case ENDFILE: //Fin d'envoi du fichier donc on revient sur le menu si on est sur un client
				printf("Fin de la séquence\n");				
				break;
			case END: //Fin d'envoi du fichier donc on revient sur le menu si on est sur un client
				if(t->orig == SERVER)
				{
					decoClient();
				}
				else
				{
					// On met à jour le traçage des trames du serveur à 0
					reinitServeur();
					
					printf("Un client vient de se déconnecter : %d\n", num_trame);
				}
				printf("Fin de la séquence\n");				
				break;
			default:
				printf("Type inconnu.");
				break;
		}
	}                         
              
}

//On initialise les valeurs de notre trame
trame* creerTrame(char* msg, char* name, int tailleRe, origine Orig){
        trame * t;
        t = malloc(sizeof(*t) + strlen(msg) + strlen(name) +1);
        //printf("On incrémente le num trame de %d",num_trame);
        t->i = num_trame;
        num_trame = num_trame+1;
        //printf(" a %d\n",num_trame); 
        t->taille = tailleRe;
		t->orig = Orig;
        strcpy(t->mess, msg);
		strcpy(t->name, name);
        return t;
}

/*
 * Fonction qui permet d'envoyer une trame avec l'instruction
 */
void envoyerTrame(trame* t, instruction type){
        t->type = type;
        //on envoie deux fois notre trame pour limiter le nombre de perte
        if(sendto(sockfd,t,sizeof(*t) + strlen(t->mess) + strlen(t->name) +1,0,(struct sockaddr *)to,addr_in_size) == -1){
                printf("Error ");
                exit(-1);
        }
        if(sendto(sockfd,t,sizeof(*t) + strlen(t->mess) + strlen(t->name) +1,0,(struct sockaddr *)to,addr_in_size) == -1){
                printf("Error ");
                exit(-1);
        }
}

//On recoit la trame, on la soumet à la gestion de flux puis à l'interprétation via la fonction protocole
void recevoirTrame(){
	int cc;
	char* r[1000];
	
	while(1){
		if((cc = recvfrom (sockfd,r,sizeof(r),0,(struct sockaddr *)to,&addr_in_size)) == -1){
				printf("Error ");
				exit(-1);
		};
		trame* t = (trame *) r;
		protocole(gererFlux(t));
	}
}

//Initialisation du serveur
int initServer(u_short port){
	addr_in_size = sizeof(struct sockaddr_in);
	from = malloc(addr_in_size);
	to = malloc(sizeof(struct sockaddr_in));
	my_addr = malloc(addr_in_size);

	memset(my_addr,0,addr_in_size);
	my_addr->sin_family = AF_INET;
	my_addr->sin_addr.s_addr = htonl(INADDR_ANY);
	my_addr->sin_port = htons(port);

	if((sockfd = socket (PF_INET, SOCK_DGRAM, 0)) < 0){
			printf("Erreur lors de la creation du socket.");
			exit(-1);
	};

	if(bind(sockfd, (struct sockaddr *)my_addr, addr_in_size) < 0){
			printf("Erreur de binding de la socket. Le socket est-il deja bindé par un serveur existant ?");
			exit(-1);
	};

	fprintf(stdout,"PRET !\n");
	return (12345);
}


// Initialisation du client
void initClient(u_short port, char* hostName){
	// Déclaration des variables
	struct hostent *toinfo;
	u_long toaddr;
	
	// Allocation mémoire pour les variables de type sockaddr_in
	to = malloc(sizeof(struct sockaddr_in));
	from = malloc(sizeof(struct sockaddr_in));
	
	// On récupère les infos sur le serveur via son adresse ip
	if((toinfo = gethostbyname(hostName)) == NULL){
			printf("Error toinfo\n");
			exit(-1);
	};
	toaddr = *(u_long *)toinfo->h_addr_list[0];
	
	// On met à 0 notre variable caractérisant le serveur
	addr_in_size = sizeof(struct sockaddr_in);
	memset(to,0,addr_in_size);
	
	// On remplit notre variable serveur avec des données réseau
	to->sin_family = AF_INET;
	to->sin_addr.s_addr = toaddr;
	to->sin_port = htons(port);

	// On ouvre notre socket UDP correspondant à la liaison du client avec le serveur
	if((sockfd = socket (PF_INET, SOCK_DGRAM, 0)) < 0){
		printf("Erreur lors de la connexion avec le serveur\n");
		exit(-1);
	};
}

void decoClient()
{
	char choix = '\0';
	printf("Fin de la séquence, retour au menu ? (O/N)\n");

	// On purge le scanf
	while((choix=getchar()) != '\n' && choix != EOF)
	{
	}

	scanf("%c", &choix);
	if(choix == 'O')
	{
		menu();
	}
	else
	{
		// On envoit au serveur que l'on se déconnecte
		envoyerTrame(creerTrame("End Connexion","End Connexion",0, CLIENT), END);
		exit(0);
	}
}

// Menu
void menu(){
	// Déclaration des variables de fonctionnement du menu
	char fichChoix[30];
	int status = 0;
	int choix = 0;
	
	// Affichage du menu
	system("clear");
	printf("\t\t===        MENU          ===\n\n");
	printf("\t1. Récupérer un fichier\n");
	printf("\t2. Envoyer un fichier\n");
	printf("\t3. Sortir du programme\n");
	printf("Votre choix : ");
	// Attente de la réponse utilisateur
	scanf("%d", &choix);
	
	switch(choix){
	case 1:
		if(!(pid=fork())){
			//On crée un fils pour demander au serveur de nous envoyer la liste des fichiers disponibles
			envoyerTrame(creerTrame("","",0, CLIENT), LISTSRV);
		}else{
			//On attends la fin du listage pour demander à l'utilisateur de choisir un fichier
			wait(NULL);
			printf("Vous avez choisi de recuperer un fichier, saisissez le nom du fichier : ");
			scanf("%s",fichChoix);
			envoyerTrame(creerTrame(fichChoix,fichChoix,0, CLIENT), GET);
		}

		break;

	case 2:
			//On attends la fin du listage pour demander à l'utilisateur de choisir un fichier
			wait(NULL);
			listeContenuClient();
			printf("Vous avez choisi d'envoyer un fichier, saisissez le nom du fichier : ");
			scanf("%s",fichChoix);
			//envoyerTrame(creerTrame(fichChoix,fichChoix,0,CLIENT), GET);
			sendFichier(fichChoix);
			menu();
		break;

	case 3:
		exit(0);
		break;
	default:
		exit(0);
		printf("Choix inconnu\n");
	}
}

